//
//  BGOPersistenceManager.m
//  Bango iOS API
//
//  Created by William Shakour on September 2, 2011.
//  Copyrights © 2011 Bango - http://www.bango.net/ - All rights reserved.
//

#import "BGOPersistenceManager.h"
#import "BGODatabaseConstants.h"
#import "FMDatabase.h"
#import "BGOProfile.h"
#import "BGOEvent.h"
#import "BGOEventParameter.h"

#define NIL_IS_ES(a) a==nil?@"":a

@interface BGOPersistenceManager (Private)
-(NSString *)applicationDocumentsDirectory;
-(NSString *)applicationCacheDirectory;
-(void)addEventsToProfileForPost:(BGOProfile *)profile chunked:(BOOL)inChunks;
//-(void)test;
@end

@implementation BGOPersistenceManager

@dynamic profileTableName;
@dynamic eventTableName;
@dynamic eventParameterTableName;
@dynamic databaseName;
@dynamic debugLog;
@synthesize postChunkSize = _postChunkSize;

//-(void)test {
//  [self logProfileWithSdkType:1
//                   sdkVersion:@"version"
//                applicationId:@"test"
//              applicationType:@"application type"
//           applicationChannel:@"application channel"
//           applicationVersion:@"application version"
//                       userId:@"user id"
//           sessionRestartTime:[NSDate date]
//               connectionType:1
//                    isRoaming:NO
//               locationSource:@"Some"
//             locationAltitude:10
//             locationLatitude:20
//            locationLongitude:10
//             locationAccuracy:10
//                  netOperator:@"operator"
//                 operatorName:@"operator name"
//           operatorCountryIso:@"UK"
//                  simOperator:@"simop"
//                        brand:@"brand"
//                       device:@"device"
//                        model:@"model"
//                      product:@"product"
//                      version:@"version"
//                      buildId:@"buildId"
//       operatorConnectionType:1];
//  
//  NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:2];
//  [dictionary setObject:@"test" forKey:@"test"];
//  [dictionary setObject:@"test1" forKey:@"test1"];
//  
//  int eventId = [self logEventWithConnectionType:1
//                          operatorConnectionType:1
//                                      errorLevel:0
//                                       eventName:@"event name"
//                                     eventDetail:@"event detail"
//                                      eventValue:@"event value"
//                                         errorId:@"error id"
//                                       eventTime:[NSDate date]
//                                  eventLocalTime:[NSDate date]
//                                 eventParameters:dictionary];
//  NSLog(@"%d", eventId);  
//}

-(id)init {
  if ((self = [super init]) != nil) {
    _database = [[FMDatabase databaseWithPath:self.databaseName] retain];
    _database.shouldCacheStatements = YES;
    _persistDatabaseConnection = YES;
    
    [self openDatabaseWithForce:YES];
    _postChunkSize = DEFAULT_POST_CHUNK_SIZE;
  }
  
  return self;
}

-(void)setDebugLog:(BOOL)debugLog {
  _database.logsErrors = debugLog;
  _database.traceExecution = debugLog;
}

-(BOOL)isDebugLog {
  return _database.logsErrors || _database.traceExecution;
}

-(NSString *)profileTableName {
  return [NSString stringWithFormat:@"%@_%d", DATABASE_TABLENAMEPROFILE, DATABASE_VERSION];
}

-(NSString *)eventTableName {
  return [NSString stringWithFormat:@"%@_%d", DATABASE_TABLENAMEEVENT, DATABASE_VERSION];
}

-(NSString *)eventParameterTableName {
  return [NSString stringWithFormat:@"%@_%d", DATABASE_TABLENAMEEVENTPARAMETER, DATABASE_VERSION];
}

-(NSString *)databaseName {
  NSString *databaseFilePath = [[self applicationCacheDirectory] stringByAppendingPathComponent:DATABASE_NAME];

  // always log database location - at this point the _database class is nil 
  // asking it if debug is enabled is pointless
  NSLog(@"Database is located at %@", databaseFilePath);
  
  return databaseFilePath;
}

-(void)compactDatabase {
  [_database executeUpdate:@"VACUUM;"];
}

-(void)createTables {
  @synchronized(self) {
    [_database executeUpdate:[NSString stringWithFormat:DATABASE_TABLECREATEPROFILE, self.profileTableName]];
    [_database executeUpdate:[NSString stringWithFormat:DATABASE_TABLECREATEEVENT, self.eventTableName]];
    [_database executeUpdate:[NSString stringWithFormat:DATABASE_TABLECREATEEVENTPARAMETER, self.eventParameterTableName]];
    
    [self compactDatabase];
  }
}

-(void)dropTables {
  @synchronized(self) {
    [_database executeUpdate:@"PRAGMA writable_schema = 1;"];
    [_database executeUpdate:[NSString stringWithFormat:DATABASE_DELETETABLES, DATABASE_TABLENAMEPREFIX]];
    [_database executeUpdate:@"PRAGMA writable_schema = 0;"];
    
    [self compactDatabase];
    
    [_database executeUpdate:@"PRAGMA INTEGRITY_CHECK;"];
  }
}

-(void)openDatabase {
  [self openDatabaseWithForce:NO];
}

-(void)openDatabaseWithForce:(BOOL)forceOpen {
  BOOL openDb = NO;
  
  if (_database != nil) {
    if (forceOpen || !_isOpen) {
      openDb = YES;
    }
  } else { // this should never happen
    if (!_persistDatabaseConnection) {
      openDb = YES;
    }
  }
  
  @synchronized(self) {
    if (openDb) {
      _isOpen = [_database openWithFlags:SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE|SQLITE_OPEN_FULLMUTEX];
    }
    
    if (_insertProfileStatement == nil) {
      _insertProfileStatement = [[NSString stringWithFormat:DATABASE_CREATEPROFILE,
                                  self.profileTableName] retain];
    }
    
    if (_insertEventStatement == nil) {
      _insertEventStatement = [[NSString stringWithFormat:DATABASE_CREATEEVENT,
                                self.eventTableName] retain];
    }
    
    if (_insertEventParameterStatement == nil) {
      _insertEventParameterStatement = [[NSString stringWithFormat:DATABASE_CREATEEVENTPARAMETER,
                                         self.eventParameterTableName] retain];
    }
    
    // This does not create the tables every time but should really check update 
    // and remove existing tables in that case [self dropTables];
    [self createTables];
  }
}


-(void)closeDatabase
{
  if (_database != nil) {
    if (!_persistDatabaseConnection && _isOpen && !_openLogProfile &&
        !_openUpdateProfileLocation && !_openLogEvent &&
        !_openProfilesForPost && !_openEventsForPost && !_openEventsPosted &&
        !_openDeleteEventsPosted) {
      @synchronized(self) {
        _isOpen = ![_database close];
      }
    }
  }
}

-(void)logProfileWithSdkType:(int)sdkType
                  sdkVersion:(NSString *)sdkVersion
               applicationId:(NSString *)applicationId
             applicationType:(NSString *)applicationType
          applicationChannel:(NSString *)applicationChannel
          applicationVersion:(NSString *)applicationVersion
                      userId:(NSString *)userId
          sessionRestartTime:(int)sessionRestartTime
                     created:(NSDate *)created
              connectionType:(NSString *)connectionType
                   isRoaming:(BOOL)isRoaming
              locationSource:(NSString *)locationSource
            locationAltitude:(double)locationAltitude
            locationLatitude:(double)locationLatitude
           locationLongitude:(double)locationLongitude
            locationAccuracy:(double)locationAccuracy
                 netOperator:(NSString *)netOperator
                operatorName:(NSString *)operatorName
          operatorCountryIso:(NSString *)operatorCountryIso
                 simOperator:(NSString *)simOperator
                       brand:(NSString *)brand
                      device:(NSString *)device
                   ipAddress:(NSString *)ipAddress
                       model:(NSString *)model
                     product:(NSString *)product
                     version:(NSString *)version
                     buildId:(NSString *)buildId
      operatorConnectionType:(NSString *)operatorConnectionType {
  @synchronized(self) {
    [self openDatabase];
    
    _openLogProfile = YES;
    [_database executeUpdate:_insertProfileStatement, [NSNumber numberWithInt:sdkType],
     sdkVersion, NIL_IS_ES(applicationId), NIL_IS_ES(applicationType),
     NIL_IS_ES(applicationChannel), NIL_IS_ES(applicationVersion), 
     NIL_IS_ES(userId), [NSNumber numberWithInt:sessionRestartTime],
     created, connectionType, 
     [NSNumber numberWithBool:isRoaming], NIL_IS_ES(locationSource), 
     [NSNumber numberWithDouble:locationAltitude],
     [NSNumber numberWithDouble:locationLatitude],
     [NSNumber numberWithDouble:locationLongitude], 
     [NSNumber numberWithDouble:locationAccuracy],
     NIL_IS_ES(netOperator), NIL_IS_ES(operatorName),
     NIL_IS_ES(operatorCountryIso), NIL_IS_ES(simOperator), NIL_IS_ES(brand),
     NIL_IS_ES(device), NIL_IS_ES(model), NIL_IS_ES(product), NIL_IS_ES(version), 
     NIL_IS_ES(buildId), NIL_IS_ES(operatorConnectionType),
     [NSNumber numberWithDouble:0]];
    
    _profileId = (int)[_database lastInsertRowId];
    
    _openLogProfile = NO;
    
    [self closeDatabase];
  }
}

-(void)updateProfileLocationWithLocationLatitude:(double)locationLatitude
                               locationLongitude:(double)locationLongitude 
                                locationAltitude:(double)locationAltitude
                                locationAccuracy:(double)locationAccuracy {
  @synchronized(self) {
    [self openDatabase];
    
    _openUpdateProfileLocation = YES;
    [_database executeUpdate:[NSString stringWithFormat:DATABASE_UPDATEPROFILELOCATION,
                              self.profileTableName], 
     [NSNumber numberWithDouble:locationLatitude],
     [NSNumber numberWithDouble:locationLongitude], 
     [NSNumber numberWithDouble:locationAltitude],
     [NSNumber numberWithDouble:locationAccuracy],
     [NSNumber numberWithInt:_profileId]];
    
    _openUpdateProfileLocation = NO;
    
    [self closeDatabase];
  }
}

-(void)logEventWithConnectionType:(NSString *)connectionType
           operatorConnectionType:(NSString *)operatorConnectionType
                       errorLevel:(int)errorLevel
                        eventName:(NSString *)eventName
                      eventDetail:(NSString *)eventDetail
                       eventValue:(NSString *)eventValue
                          errorId:(NSString *)errorId
                        eventTime:(NSDate *)eventTime
                  eventParameters:(NSDictionary *)eventParameters
{
  int eventId = -1;
  
  @synchronized(self) {
    [self openDatabase];
    
    _openLogEvent = YES;
    
    [_database executeUpdate:_insertEventStatement,
     [NSNumber numberWithInt:_profileId], NIL_IS_ES(eventName),
     NIL_IS_ES(eventDetail), NIL_IS_ES(eventValue),
     NIL_IS_ES(errorId), eventTime, connectionType,
     NIL_IS_ES(operatorConnectionType), [NSNumber numberWithInt:errorLevel], 
     [NSNumber numberWithInt:0]];
    
    eventId = [_database lastInsertRowId];
    
    
    [self logEventParameterWithEventId:eventId eventParameters:eventParameters];
    
    _openLogEvent = NO;
    
    [self closeDatabase];
  }
}

-(void)logEventParameterWithEventId:(int)eventId
                    eventParameters:(NSDictionary *)eventParameters {
  NSString *eventParameterValue = nil;
  if (eventParameters != nil) {
    @synchronized(self) {
      for (NSString *eventParameterName in [eventParameters allKeys]) {
        eventParameterValue = [eventParameters valueForKey:eventParameterName];
        
        [_database executeUpdate:_insertEventParameterStatement, 
         [NSNumber numberWithInt:eventId], eventParameterName,
         NIL_IS_ES(eventParameterValue)];
      }
    }
  }
}

-(BOOL)updateEventsPostInProgress:(BGOProfile *)profile {
  BOOL updated = NO;
  @synchronized(self) {
    NSMutableString *profileEventIds = [NSMutableString string];
    
    for (BGOEvent *event in profile.events) {
      [profileEventIds appendFormat:@"%d,", event.id];
    }
    
    if (profileEventIds.length > 0) {
      [profileEventIds replaceCharactersInRange:NSMakeRange(profileEventIds.length - 1, 1)
                                     withString:@""];
    }
    
    [self openDatabase];
    
    _openEventsPosted = YES;
    if ([_database beginTransaction]) {
      [_database executeUpdate:[NSString stringWithFormat:DATABASE_UPDATEEVENTPOSTINPROGRESS,
                                self.eventTableName, profileEventIds]];
      [_database executeUpdate:[NSString stringWithFormat:DATABASE_UPDATEPROFILEPOSTINPROGRESS,
                                self.profileTableName], 
       [NSNumber numberWithInt:profile.id]];
      
      if ([_database commit]) {
        updated = YES;
      } else {
        NSLog(@"Error commiting trasaction");
      }
    } else {
      NSLog(@"Error beginning trasaction");
    }
    
    _openEventsPosted = NO;
    
    [self closeDatabase];
  }
  return updated;
}

-(BOOL)updateEventsPosted:(int)profileId {
  BOOL updated = NO;
  @synchronized(self) {
    [self openDatabase];
    
    _openEventsPosted = YES;
    
    if ([_database beginTransaction]) {
      NSNumber *boxedProfileId = [NSNumber numberWithInt:profileId];
      
      [_database executeUpdate:[NSString stringWithFormat:DATABASE_UPDATEEVENTPOSTED,
                                self.eventTableName], boxedProfileId];
      [_database executeUpdate:[NSString stringWithFormat:DATABASE_UPDATEPROFILEPOSTED,
                                self.profileTableName, self.eventTableName], 
       boxedProfileId, boxedProfileId];
      
      if ([_database commit]) {
        updated = YES;
      } else {
        NSLog(@"Error commiting trasaction");
      }
    } else {
      NSLog(@"Error beginning trasaction");
    }
    
    _openEventsPosted = NO;
    
    [self closeDatabase];
  }
  
  return updated;
}

-(BOOL)deleteEventsPosted:(int)profileId {
  BOOL deleted = NO;
  @synchronized(self) {
    [self openDatabase];
    
    _openDeleteEventsPosted = YES;
    
    if ([_database beginTransaction]) {
      [_database executeUpdate:[NSString stringWithFormat:DATABASE_DELETEEVENTPARAMETER,
                                self.eventParameterTableName, self.eventTableName],
       [NSNumber numberWithInt:profileId]];
      [_database executeUpdate:[NSString stringWithFormat:DATABASE_DELETEEVENT,
                                self.eventTableName], 
       [NSNumber numberWithInt:profileId]];
      
      if (![self isCurrentProfile:profileId]) {
        [_database executeUpdate:[NSString stringWithFormat:DATABASE_DELETEPROFILE, 
                                  self.profileTableName], 
         [NSNumber numberWithInt:profileId]];
      }
      
      if ([_database commit]) {
        deleted = YES;
      } else {
        NSLog(@"Error commiting trasaction");
      }
    } else {
      NSLog(@"Error beginning trasaction");
    }
    
    _openDeleteEventsPosted = NO;
    
    [self closeDatabase];
  }
  
  return deleted;
}

-(NSArray *)getProfilesForPost:(BOOL)notCurrentProfile {
  return [self getProfilesForPost:notCurrentProfile chunked:NO];
}

-(NSArray *)getProfilesForPost:(BOOL)notCurrentProfile chunked:(BOOL)inChuncks {
  int notProfileId = 0;
  
  NSMutableArray *returnValue = [NSMutableArray array];
  BGOProfile *bangoProfile;
  
  @synchronized(self) {
    if (notCurrentProfile) {
      notProfileId = _profileId;
    }
    
    [self openDatabase];
    _openProfilesForPost = true;
    
    FMResultSet *profiles = [_database executeQuery:[NSString stringWithFormat:DATABASE_SELECTPOSTPROFILES,
                                                     self.profileTableName],
                             [NSNumber numberWithInt:notProfileId]];
    
    if (profiles != nil) {
      while ([profiles next]) {
        bangoProfile = [BGOProfile profile];
        bangoProfile.id = [profiles intForColumn:DATABASE_COLUMN_PROFILEID];
        bangoProfile.sdkType = [profiles intForColumn:DATABASE_COLUMN_SDKTYPE];
        bangoProfile.sdkVersion = [profiles stringForColumn:DATABASE_COLUMN_SDKVERSION];
        bangoProfile.applicationKey = [profiles stringForColumn:DATABASE_COLUMN_APPLICATIONID];
        bangoProfile.applicationType = [profiles stringForColumn:DATABASE_COLUMN_APPLICATIONTYPE];
        bangoProfile.applicationChannel = [profiles stringForColumn:DATABASE_COLUMN_APPLICATIONCHANNEL];
        bangoProfile.applicationVersion = [profiles stringForColumn:DATABASE_COLUMN_APPLICATIONVERSION];
        bangoProfile.userId = [profiles stringForColumn:DATABASE_COLUMN_USERID];
        bangoProfile.sessionRestartTime = [profiles intForColumn:DATABASE_COLUMN_SESSIONRESTARTTIME];
        bangoProfile.created = [profiles dateForColumn:DATABASE_COLUMN_CREATED];
        bangoProfile.connectionType = [profiles stringForColumn:DATABASE_COLUMN_CONNECTIONTYPE];
        bangoProfile.isRoaming = [profiles intForColumn:DATABASE_COLUMN_ISROAMING] == 0 ? NO : YES;
        bangoProfile.locationSource = [profiles stringForColumn:DATABASE_COLUMN_LOCATIONSOURCE];
        bangoProfile.locationAltitude = [profiles doubleForColumn:DATABASE_COLUMN_LOCATIONALTITUDE];
        bangoProfile.locationLatitude = [profiles doubleForColumn:DATABASE_COLUMN_LOCATIONLATITUDE];
        bangoProfile.locationLongitude = [profiles doubleForColumn:DATABASE_COLUMN_LOCATIONLONGITUDE];
        bangoProfile.locationAccuracy = [profiles doubleForColumn:DATABASE_COLUMN_LOCATIONACCURACY];
        bangoProfile.networkOperator = [profiles stringForColumn:DATABASE_COLUMN_OPERATOR];
        bangoProfile.operatorName = [profiles stringForColumn:DATABASE_COLUMN_OPERATORNAME];
        bangoProfile.operatorCountryIso = [profiles stringForColumn:DATABASE_COLUMN_OPERATORCOUNTRYISO];
        bangoProfile.sim = [profiles stringForColumn:DATABASE_COLUMN_SIMOPERATOR];
        bangoProfile.brand = [profiles stringForColumn:DATABASE_COLUMN_BRAND];
        bangoProfile.device = [profiles stringForColumn:DATABASE_COLUMN_DEVICE];
        bangoProfile.model = [profiles stringForColumn:DATABASE_COLUMN_MODEL];
        bangoProfile.product = [profiles stringForColumn:DATABASE_COLUMN_PRODUCT];
        bangoProfile.version = [profiles stringForColumn:DATABASE_COLUMN_VERSION];
        bangoProfile.buildId = [profiles stringForColumn:DATABASE_COLUMN_BUILDID];
        bangoProfile.operatorConnectionType = [profiles stringForColumn:DATABASE_COLUMN_OPERATORCONNECTIONTYPE];
        bangoProfile.isProfilePosted = [profiles intForColumn:DATABASE_COLUMN_PROFILEPOSTED] == 0 ? NO : YES;
        
        [self addEventsToProfileForPost:bangoProfile chunked:inChuncks];
        
        [returnValue addObject:bangoProfile];
      }
    }
    
    [profiles close];
    
    
    _openProfilesForPost = false;
    
    [self closeDatabase];
  }
  
  return returnValue;
}
  
-(void)addEventsToProfileForPost:(BGOProfile *)profile chunked:(BOOL)inChunks {
  BGOEvent *bangoEvent;
  BGOEventParameter *bangoEventParameter;
  FMResultSet *events = nil;
  FMResultSet *eventParameters = nil;
  
  @synchronized(self) {
    [self openDatabase];
    
    _openEventsForPost = true;
    
    events = [_database executeQuery:[NSString stringWithFormat:inChunks ? DATABASE_SELECTPOSTEVENTS : DATABASE_SELECTPOSTEVENTS_ALL,
                                      self.eventTableName, 
                                      _postChunkSize <= MIN_POST_CHUNK_SIZE || _postChunkSize > MAX_POST_CHUNK_SIZE ? DEFAULT_POST_CHUNK_SIZE : _postChunkSize],
              [NSNumber numberWithInt:profile.id]];
    
    if (events != nil) {
      while ([events next]) {
        bangoEvent = [BGOEvent event];
        bangoEvent.id = [events intForColumn:DATABASE_COLUMN_EVENTID];
        bangoEvent.eventName = [events stringForColumn:DATABASE_COLUMN_EVENTNAME];
        bangoEvent.eventDetail = [events stringForColumn:DATABASE_COLUMN_EVENTDETAIL];
        bangoEvent.eventValue = [events stringForColumn:DATABASE_COLUMN_EVENTVALUE];
        bangoEvent.errorId = [events stringForColumn:DATABASE_COLUMN_ERRORID];
        bangoEvent.eventTime = [NSDate dateWithTimeIntervalSince1970:[events longForColumn:DATABASE_COLUMN_EVENTTIME]];
        bangoEvent.connectionType = [events stringForColumn:DATABASE_COLUMN_CONNECTIONTYPE];
        bangoEvent.operatorConnectionType = [events stringForColumn:DATABASE_COLUMN_OPERATORCONNECTIONTYPE];
        bangoEvent.errorLevel = [events intForColumn:DATABASE_COLUMN_ERRORLEVEL];
        bangoEvent.eventPosted = [events intForColumn:DATABASE_COLUMN_EVENTPOSTED] == 0 ? NO : YES;
        
        eventParameters = [_database executeQuery:[NSString stringWithFormat:DATABASE_SELECTPOSTEVENTPARAMETERS, self.eventParameterTableName],
                           [NSNumber numberWithInt:bangoEvent.id]];
        if (eventParameters != nil) {
          while ([eventParameters next]) {
            bangoEventParameter = [BGOEventParameter eventParameter];
            bangoEventParameter.id = [events intForColumn:DATABASE_COLUMN_EVENTID];
            bangoEventParameter.eventName = [eventParameters stringForColumn:DATABASE_COLUMN_EVENTNAME];
            bangoEventParameter.eventValue = [eventParameters stringForColumn:DATABASE_COLUMN_EVENTVALUE];
            [bangoEvent addToEventParameters:bangoEventParameter];
          }
        }
        
        [eventParameters close];
        
        [profile addToEvents:bangoEvent];
      }
      
      
      [events close];
    }
    
    [self closeDatabase];
  }
}

-(BOOL)isCurrentProfile:(int)profileId {
  return _profileId == profileId;
}

-(void)dealloc {
  [_database release];
  [_insertProfileStatement release];
  [_insertEventStatement release];
  [_insertEventParameterStatement release];
  [super dealloc];
}

#pragma mark - Private methods

-(NSString *)applicationDocumentsDirectory {
  return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                              NSUserDomainMask, YES)
          lastObject];
}

- (NSString *)applicationCacheDirectory {
  return [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
                                              NSUserDomainMask, YES)
          lastObject];
}

#pragma mark - shared persistence manager

static BGOPersistenceManager *sharedPersistenceManager = nil; 

+ (BGOPersistenceManager *)sharedPersistenceManager
{ 
	@synchronized(self) { 
		if (sharedPersistenceManager == nil) { 
			sharedPersistenceManager = [[self alloc] init]; 
		} 
	} 
  
	return sharedPersistenceManager; 
} 

+ (id)allocWithZone:(NSZone *)zone 
{ 
	@synchronized(self) { 
		if (sharedPersistenceManager == nil) { 
			sharedPersistenceManager = [super allocWithZone:zone]; 
			return sharedPersistenceManager; 
		} 
	} 
  
	return nil; 
} 

- (id)copyWithZone:(NSZone *)zone 
{ 
	return self; 
} 

- (id)retain 
{ 
	return self; 
} 

- (NSUInteger)retainCount 
{ 
	return NSUIntegerMax; 
} 

- (oneway void)release 
{ 
} 

- (id)autorelease 
{ 
	return self; 
}

@end
